<?php
/**
 * @link https://www.kancloud.cn/cleverstone/ymb2
 * @copyright Copyright (c) 2020 Yii Manager Software LLC
 */

namespace builder\base;

use Yii;
use yii\bootstrap4\Alert;
use yii\db\Query;
use yii\db\StaleObjectException;
use yii\web\Response;
use builder\helper\App;
use yii\db\ActiveQuery;
use yii\db\ActiveRecord;
use yii\helpers\StringHelper;
use yii\db\ExpressionInterface;
use yii\base\UnknownMethodException;
use builder\behaviors\DatetimeBehavior;

/**
 * 模型继承类
 * @property-read string $error 验证失败，错误提示语。
 * @method static BaseActiveRecord findOneThrowEmptyException(mixed $condition) 指定条件查询一条数据，返回为空时，抛出`Not Content`异常。(禁止在try..catch..中使用)
 * @method static BaseActiveRecord findOneWithOperatorThrowEmptyException(mixed $condition, $fields = null, array $conditionParams = []) 指定条件使用`limit`方法查询一条数据（支持操作符，不支持传入主键值），返回为空时，抛出`Not Content`异常。(禁止在try..catch..中使用)
 * @method static array queryOneWithLimitThrowEmptyException(mixed $condition, $fields = '*', array $conditionParams = []) 指定条件使用`limit`方法查询一条数据（支持操作符，不支持传入主键值），返回为空时，抛出`Not Content`异常。(禁止在try..catch..中使用)
 * @method static BaseActiveRecord[] findAllThrowEmptyException(mixed $condition) 指定条件查询所有数据，返回为空时，抛出`Not Content`异常。(禁止在try..catch..中使用)
 * @method static BaseActiveRecord[] findAllWithOperatorThrowEmptyException(mixed $condition, $fields = null, array $conditionParams = []) 指定条件查询所有数据（支持操作符，不支持传入主键值），返回为空时，抛出`Not Content`异常。(禁止在try..catch..中使用)
 * @method static array queryAllThrowEmptyException(mixed $condition, $fields = '*', array $conditionParams = []) 指定条件查询所有数据（支持操作符，不支持传入主键值），返回为空时，抛出`Not Content`异常。(禁止在try..catch..中使用)
 *
 * @author cleverstone
 * @since ym2.0
 */
class BaseActiveRecord extends ActiveRecord
{
    /**
     * 混入
     * Returns a list of behaviors that this component should behave as.
     *
     * Child classes may override this method to specify the behaviors they want to behave as.
     *
     * The return value of this method should be an array of behavior objects or configurations
     * indexed by behavior names. A behavior configuration can be either a string specifying
     * the behavior class or an array of the following structure:
     *
     * ```php
     * 'behaviorName' => [
     *     'class' => 'BehaviorClass',
     *     'property1' => 'value1',
     *     'property2' => 'value2',
     * ]
     * ```
     *
     * Note that a behavior class must extend from [[Behavior]]. Behaviors can be attached using a name or anonymously.
     * When a name is used as the array key, using this name, the behavior can later be retrieved using [[getBehavior()]]
     * or be detached using [[detachBehavior()]]. Anonymous behaviors can not be retrieved or detached.
     *
     * Behaviors declared in this method will be attached to the component automatically (on demand).
     *
     * @return array the behavior configurations.
     */
    public function behaviors()
    {
        return [
            // 日期自动写入
            'timestampBehavior' => [
                'class' => DatetimeBehavior::class,
                'attributes' => [
                    self::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
                    self::EVENT_BEFORE_UPDATE => ['updated_at'],
                ],
            ],
        ];
    }

    /**
     * ‌禁用 ONLY_FULL_GROUP_BY 模式（仅影响当前会话）
     * @return void
     */
    public static function removeOnlyFullGroupBySqlModeFromCurrentSession()
    {
        self::getDb()->createCommand('SET sql_mode = REPLACE(@@sql_mode, "ONLY_FULL_GROUP_BY", "")')->execute();
    }

    /**
     * Saves the changes to this active record into the associated database table.
     *
     * This method performs the following steps in order:
     *
     * 1. call [[beforeValidate()]] when `$runValidation` is `true`. If [[beforeValidate()]]
     *    returns `false`, the rest of the steps will be skipped;
     * 2. call [[afterValidate()]] when `$runValidation` is `true`. If validation
     *    failed, the rest of the steps will be skipped;
     * 3. call [[beforeSave()]]. If [[beforeSave()]] returns `false`,
     *    the rest of the steps will be skipped;
     * 4. save the record into database. If this fails, it will skip the rest of the steps;
     * 5. call [[afterSave()]];
     *
     * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]],
     * [[EVENT_AFTER_VALIDATE]], [[EVENT_BEFORE_UPDATE]], and [[EVENT_AFTER_UPDATE]]
     * will be raised by the corresponding methods.
     *
     * Only the [[dirtyAttributes|changed attribute values]] will be saved into database.
     *
     * For example, to update a customer record:
     *
     * ```php
     * $customer = Customer::findOne($id);
     * $customer->name = $name;
     * $customer->email = $email;
     * $customer->updateByAndWhere(true, ['status' => 1]);
     * ```
     *
     * Note that it is possible the update does not affect any row in the table.
     * In this case, this method will return 0. For this reason, you should use the following
     * code to check if update() is successful or not:
     *
     * ```php
     * if ($customer->updateByAndWhere(true, ['status' => 1]) !== false) {
     *     // update successful
     * } else {
     *     // update failed
     * }
     * ```
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
     * being updated is outdated.
     * @throws \Throwable in case update failed.
     */
    public function updateByAndWhere($runValidation = true, $addCondition = [], $params = [], $attributeNames = null)
    {
        return $this->updateBy($runValidation, $addCondition, $params, 'and', $attributeNames);
    }

    /**
     * @see updateByAndWhere
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
     * being updated is outdated.
     * @throws \Throwable in case update failed.
     */
    public function updateByOrWhere($runValidation = true, $addCondition = [], $params = [], $attributeNames = null)
    {
        return $this->updateBy($runValidation, $addCondition, $params, 'or', $attributeNames);
    }

    /**
     * @see updateByOrWhere
     * @see updateByAndWhere
     *
     * @param bool $runValidation whether to perform validation (calling [[validate()]])
     * before saving the record. Defaults to `true`. If the validation fails, the record
     * will not be saved to the database and this method will return `false`.
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array $operator operator for conditions.
     * @param array|null $attributeNames list of attributes that need to be saved. Defaults to `null`,
     * meaning all attributes that are loaded from DB will be saved.
     * @return int|false the number of rows affected, or false if validation fails
     * or [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data
     * being updated is outdated.
     * @throws \Throwable in case update failed.
     */
    protected function updateBy($runValidation = true, $addCondition = [], $params = [], $operator = 'and', $attributeNames = null)
    {
        if ($runValidation && !$this->validate($attributeNames)) {
            Yii::info('Model not updated due to validation error.', __METHOD__);
            return false;
        }

        if (!$this->isTransactional(self::OP_UPDATE)) {
            return $this->updateByInternal($addCondition, $params, $operator, $attributeNames);
        }

        $transaction = static::getDb()->beginTransaction();
        try {
            $result = $this->updateByInternal($addCondition, $params, $operator, $attributeNames);
            if ($result === false) {
                $transaction->rollBack();
            } else {
                $transaction->commit();
            }

            return $result;
        } catch (\Exception $e) {
            $transaction->rollBack();
            throw $e;
        } catch (\Throwable $e) {
            $transaction->rollBack();
            throw $e;
        }
    }

    /**
     * @see updateBy()
     *
     * @param array $addCondition the condition that will be put in the WHERE part. Please
     * refer to [[Query::where()]] on how to specify condition.
     * @param array $params the parameters (name => value) to be bound to the query.
     * @param array $operator operator for conditions.
     * @param array|null $attributes attributes to update
     * @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
     * @throws StaleObjectException
     */
    protected function updateByInternal($addCondition = [], $params = [], $operator = 'and', $attributes = null)
    {
        if (!$this->beforeSave(false)) {
            return false;
        }
        $values = $this->getDirtyAttributes($attributes);
        if (empty($values)) {
            $this->afterSave(false, $values);
            return 0;
        }
        $condition = $this->getOldPrimaryKey(true);
        $lock = $this->optimisticLock();
        if ($lock !== null) {
            $values[$lock] = $this->$lock + 1;
            $condition[$lock] = $this->$lock;
        }
        // We do not check the return value of updateAll() because it's possible
        // that the UPDATE statement doesn't change anything and thus returns 0.
        $rows = static::updateAll($values, [$operator, $condition, $addCondition], $params);

        if ($lock !== null && !$rows) {
            throw new StaleObjectException('The object being updated is outdated.');
        }

        if (isset($values[$lock])) {
            $this->$lock = $values[$lock];
        }

        $changedAttributes = [];
        foreach ($values as $name => $value) {
            $changedAttributes[$name] = $this->getOldAttribute($name);
            $this->setOldAttribute($name, $value);
        }
        $this->afterSave(false, $changedAttributes);

        return $rows;
    }

    /**
     * `ActiveQuery`中的模型关联查询方法
     *
     * Example 1 (延迟加载)
     * ```php
     * // SELECT * FROM `customer` WHERE `id` = 123
     * $customer = Customer::findOne(123);
     *
     * // SELECT * FROM `order` WHERE `customer_id` = 123
     * $orders = $customer->orders;
     *
     * // 没有 SQL 语句被执行
     * $orders2 = $customer->orders;
     * ```
     *
     *
     *  Example 2 (即时加载)
     * ```php
     * // SELECT * FROM `customer` LIMIT 100;
     * // SELECT * FROM `orders` WHERE `customer_id` IN (...)
     * $customers = Customer::find()->with('orders')->limit(100)->all();
     * foreach ($customers as $customer) {
     *      // 没有任何的 SQL 执行
     *      $orders = $customer->orders;
     * }
     *
     * ```
     *
     *
     * Example 3 (关联查询(自定义关联关系))
     * ```php
     * // 1、SELECT `customer`.* FROM `customer` LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` WHERE `order`.`status` = 1
     * // 2、SELECT * FROM `order` WHERE `customer_id` IN (...)
     * $customers = Customer::find()
     *      ->select('customer.*') //必须查询主表字段，且必须含有关联表字段。（查询1）
     *      ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`') //实际上可以看作主表的查询条件。（查询1）
     *      ->where(['order.status' => 1]) //主查询条件对主表数据的过滤。（查询1）
     *      ->with([
     *          'orders' => function ($db) {
     *              // 关联表`in`查询的条件和字段。（查询2）
     *              // 其中 alias()、onCondition()、where()等方法也将被主查询使用。（查询1）
     *              // 和Example 5 (关联模型声明)位置不同，功能是大同小异的。
     *              $query->andWhere(['>', 'subtotal', 100])->select('id,order_num,status');
     *          },
     *      ]) //该方法必须调用（底层是对关联表的`in`查询），并且创建关联数据模型。（查询2）
     *      ->all();
     * ```
     *
     *
     * Example 4 (关联查询（利用已存在的关联声明）)
     * ```php
     * // 1、SELECT `customer`.* FROM `customer` LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` WHERE `order`.`status` = 1
     * // 2、SELECT * FROM `order` WHERE `customer_id` IN (...)
     * $customers = Customer::find()
     *      ->joinWith('orders')
     *      ->where(['order.status' => 1]) //主查询条件对主表数据的过滤。（查询1）
     *      ->all();
     *
     * ```
     *
     *
     * Example 5 (关联模型声明)
     * ```php
     * //当前模型 `Customer`
     * public function getOrders()
     * {
     *     //1. get* 如果一对多，*为复数。如：getOrders
     *     //2. hasMany 参数一为关联模型的类名。
     *     //3. hasMany 参数二是一个数组，数组的key为关联模型表中字段、value为主模型表中的字段。
     *     //4. alias 方法为关联表的别名。
     *     //5. onCondition 为主查询额外添加关联条件，并作为关联表的in查询条件。
     *
     *     //1、SELECT `customer`.* FROM `customer` LEFT JOIN `order` `o` ON (`o`.`id` = `customer`.`order_id`) AND (o.status=1)
     *     //2、SELECT * FROM `order` `o` WHERE (`id` IN ('1', '2', '3', '4', '5')) AND (o.status=1)
     *     return $this->hasMany(Order::class, ['id' => 'order_id'])->alias('o')->onCondition('o.status=1');
     * }
     *
     * ```
     *
     * @param null|string|array|ExpressionInterface $fields the columns to be selected.
     * @param string|null $selectOption additional option that should be appended to the 'SELECT' keyword. For example, in MySQL, the option 'SQL_CALC_FOUND_ROWS' can be used.
     * @return ActiveQuery
     */
    public static function activeQuery($fields = null, $selectOption = null)
    {
        $activeQuery = static::find();
        if (empty($fields)) {
            return $activeQuery;
        } else {
            return $activeQuery->select($fields, $selectOption);
        }
    }

    /**
     * 对`findOne()`的优化和扩展，支持复杂的操作符条件(不支持直接传入主键值或主键值数组)，并且使用了`limit`查询优化。
     *
     * ```php
     *
     * // return ActiveRecord | null
     * User::findOneWithOperator([
     *     'or',
     *     ['name' => 'Tom'],
     *     [
     *         'and',
     *         ['sex' => 1],
     *         ['>=', 'age', 18],
     *     ],
     * ]);
     *
     * // return ActiveRecord | throw exception
     * User::findOneWithOperator([
     *     'or',
     *     ['name' => 'Tom'],
     *     [
     *         'and',
     *         ['sex' => 1],
     *         ['>=', 'age', 18],
     *     ],
     * ]);
     *
     * ```
     *
     * @param string|array|ExpressionInterface $condition the conditions that should be put in the WHERE part.
     * @param null|string|array|ExpressionInterface $fields the columns to be selected.
     * @param array $conditionParams the parameters (name => value) to be bound to the query.
     * @return BaseActiveRecord|null
     */
    public static function findOneWithOperator($condition, $fields = null, array $conditionParams = [])
    {
        return static::activeQuery($fields)->where($condition, $conditionParams)->limit(1)->one();
    }

    /**
     * 对`findAll()`的扩展，支持复杂的操作符条件(不支持直接传入主键值或主键值数组)。
     *
     * ```php
     *
     * // return array
     * User::findAllWithOperator([
     *     'or',
     *     ['name' => 'Tom'],
     *     [
     *         'and',
     *         ['sex' => 1],
     *         ['>=', 'age', 18],
     *     ],
     * ]);
     *
     * // return array | throw exception
     * User::findAllWithOperatorThrowEmptyException([
     *     'or',
     *     ['name' => 'Tom'],
     *     [
     *         'and',
     *         ['sex' => 1],
     *         ['>=', 'age', 18],
     *     ],
     * ]);
     *
     * ```
     *
     * @param string|array|ExpressionInterface $condition the conditions that should be put in the WHERE part.
     * @param null|string|array|ExpressionInterface $fields the columns to be selected.
     * @param array $conditionParams the parameters (name => value) to be bound to the query.
     * @return BaseActiveRecord[]
     */
    public static function findAllWithOperator($condition, $fields = null, array $conditionParams = [])
    {
        return static::activeQuery($fields)->where($condition, $conditionParams)->all();
    }

    /**
     * 重构find方法
     * @return ActiveQuery
     */
    public static function find()
    {
        $activeQueryClass = get_called_class() . 'Query';

        if (class_exists($activeQueryClass)) {
            $activeQueryInstance = Yii::createObject($activeQueryClass, [get_called_class()]);
            if ($activeQueryInstance instanceof ActiveQuery) {
                return $activeQueryInstance;
            }
        }

        return parent::find();
    }

    /**
     * 获取db中的`Query`对象[关联查询，不用声明关联关系] (强烈推荐使用该方法)
     *
     * @param array|string $fields 查询字段
     * @param string|string 表别名
     * @return Query
     * @throws
     */
    public static function query($fields = '*', $alias = null)
    {
        /* @var Query $query */
        $query = new Query();

        if (empty($alias)) {
            return $query->from(static::tableName())->select($fields);
        }

        return $query->from([$alias => static::tableName()])->select($fields);
    }

    /**
     * 用`\yii\db\Query`中[[ limit ]]方法查询一条数据。
     *
     * ```php
     *
     * // return array|false
     * User::queryOneWithLimit([
     *     'or',
     *     ['name' => 'Tom'],
     *     [
     *         'and',
     *         ['sex' => 1],
     *         ['>=', 'age', 18],
     *     ],
     * ]);
     *
     * // return array | throw exception
     *  User::queryOneWithLimitThrowEmptyException([
     *      'or',
     *      ['name' => 'Tom'],
     *      [
     *          'and',
     *          ['sex' => 1],
     *          ['>=', 'age', 18],
     *      ],
     *  ]);
     *
     * ```
     *
     * @param string|array|ExpressionInterface $condition the conditions that should be put in the WHERE part.
     * @param string|array|ExpressionInterface $fields the columns to be selected.
     * @param array $conditionParams the parameters (name => value) to be bound to the query.
     * @return array|false
     */
    public static function queryOneWithLimit($condition, $fields = '*', array $conditionParams = [])
    {
        return static::query($fields)->where($condition, $conditionParams)->limit(1)->one();
    }

    /**
     * 用`\yii\db\Query`方法查询指定条件下所有数据。
     *
     * ```php
     *
     * // return array
     * User::queryAll([
     *     'or',
     *     ['name' => 'Tom'],
     *     [
     *         'and',
     *         ['sex' => 1],
     *         ['>=', 'age', 18],
     *     ],
     * ]);
     *
     * // return array | throw exception
     *  User::queryAllThrowEmptyException([
     *      'or',
     *      ['name' => 'Tom'],
     *      [
     *          'and',
     *          ['sex' => 1],
     *          ['>=', 'age', 18],
     *      ],
     *  ]);
     *
     * ```
     *
     * @param string|array|ExpressionInterface $condition the conditions that should be put in the WHERE part.
     * @param string|array|ExpressionInterface $fields the columns to be selected.
     * @param array $conditionParams the parameters (name => value) to be bound to the query.
     * @return array
     */
    public static function queryAll($condition, $fields = '*', array $conditionParams = [])
    {
        return static::query($fields)->where($condition, $conditionParams)->all();
    }

    /**
     * 获取验证失败提示[字符串格式]
     * @return string
     */
    public function getError()
    {
        $firstErrors = $this->firstErrors;
        return reset($firstErrors) ?: 'Unknown Error。';
    }

    /**
     * @param array|\ArrayAccess $data the data array to load, typically `$_POST` or `$_GET`.
     * {@inheritdoc}
     */
    public function load($data, $formName = '')
    {
        return parent::load($data, $formName);
    }

    /**
     * 当结果存在错误时，抛出异常
     * @return void
     * @throws \Exception
     */
    public function throwExceptionWhenHasError()
    {
        if ($this->hasErrors()) {
            throw new \Exception($this->error);
        }
    }

    /**
     * 拦截以`ThrowEmptyException`结尾的静态方法，并调用其原方法，如果原方法返回值为空，则抛出无效的值异常。
     *
     * @param string $name 方法ID
     * @param array $arguments 方法参数
     * @return mixed
     * @throws \yii\base\ExitException
     */
    public static function __callStatic($name, $arguments)
    {
        $keyStr = 'ThrowEmptyException';

        if (StringHelper::endsWith($name, $keyStr)) {
            $staticMethod = StringHelper::byteSubstr($name, 0, 0-StringHelper::byteLength($keyStr));
            if (method_exists($calledClass = get_called_class(), $staticMethod)) {
                $result = call_user_func_array([$calledClass, $staticMethod], $arguments);
                if (empty($result)) {
                    $controller = Yii::$app->controller;
                    $response = Yii::$app->response;
                    $message = 'To call the method \\' . $calledClass . "::$staticMethod()" . ' ,But empty value be returned.';

                    if (Yii::$app->request->isAjax) {
                        $format = Response::FORMAT_JSON;
                        $responseData = [
                            'code' => App::params('http.res.failure'),
                            'data' => [],
                            'msg' => $message,
                        ];
                    } else {
                        $format = Response::FORMAT_HTML;
                        $responseData = $controller->renderContent(
                            Alert::widget([
                                'body' => $message,
                                'closeButton' => false,
                                'options' => ['class' => 'alert-danger m-3 p-3'],
                            ])
                        );
                    }

                    $response->format = $format;
                    $response->data = $responseData;

                    //正常触发 `afterAction` 事件;
                    $controller->afterAction($controller->action, $response);

                    throw new \yii\base\ExitException();
                }

                return $result;
            }
        }

        throw new UnknownMethodException('Calling unknown method: ' . get_called_class() . "::$name()");
    }
}